JVM(Java 虚拟机)各个组成详细总结

您所在的位置:网站首页 jvm 虚拟机 亿美元 JVM(Java 虚拟机)各个组成详细总结

JVM(Java 虚拟机)各个组成详细总结

2023-09-12 20:37| 来源: 网络整理| 查看: 265

JVM(Java 虚拟机) 1.简介 1.1 概述

JVM 是运行操作系统之上,没有和硬件有直接交互,但有可以通过接口调用来与底层操作系统交互。

Java 能够实现一次编译,全平台运行,就是通过 JVM 来实现。通过 JVM 可以让相同的数据类型在不同的系统上运行得到统一的结果。

1.2 组成

程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号器,用于记录当前虚拟机正在执行的线程指令地址虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出 StackOverFlowError本地方法栈:线程私有的,保存的是 native 方法的信息,当一个 jvm 创建的线程调用 native 方法 后,jvm 不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法堆:java 堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该 区域经常发生垃圾回收的操作方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据

程序计数器、虚拟机栈、本地方法栈为线程私有,堆、方法区为线程共享。

2.ClassLoder 类加载器 2.1 概述

负责将接收到的 class 文件到方法区去找相应的模版(类)进行实例化,有三种 JVM 自带的类加载器和用户自定义加载器。

加载的类信息存放于称为方法区的内存空间,常量池运行时加载到内存中,即运行时常量池(1)。

(1):在 java 1.8 以后方法区通过元空间实现,方法区存放类的信息存放到了元空间、运行时常量池存放到堆中。

运行时常量池在 Java 程序运行期间是可以动态放入新的值到常量池中,典型的方式就是 String 中的 intern() 方法。

常量池无法申请到内存空间后,会抛出 OutOfMemoryError(OOM)

2.2 类加载过程

类的整个生命周期包括:加载(Loading)、验证(Verification)、准备 (Preparation)、解析 (Resolution)、初始化 (Initialization)、使用(Using) 和 卸载(Unloading) 7个阶段。

其中准备、验证、解析3个部分统称为连接(Linking)。如图所示:

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)。

2.2.1 加载(Loading)

通过一个类的全限定名获取定义此类的二进制字节流

JVM 虚拟机并没有指明二进制字节流获取途径,因此许多如动态代理、JSP 等就可以从这里生成 class 文件,让虚拟机运行。

将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口

首先加载该类的父类

加载阶段中的加载和链接可能是交替运行

2.2.2 验证 (Verification)

验证

验证类是否符合jvm规范,安全性检查

准备

为成员(static)变量分配空间,并设置默认值(默认值不是用户指定的值,而是系统对于该变量的默认值,如:int类型的默认值为0)

空间分配和赋值两个阶段

在后面的初始化阶段(即cinit阶段,调用类的构造方法)才会进行赋值,如果加上了 final 修饰符,如:static final int a = 10;会在编译阶段就会进行赋值(基本数据类型和字符串常量适用这个规则)

注意:在 jdk6 之前是将静态变量存储在方法区,jdk7 以后是和类一起存储在堆空间中

解析

将常量池中的符号引用解析为直接引用

将代码中的符号解析的引用类型执行具体的内存地址

2.2.3 初始化 (Initialization)

初始化阶段是执行类构造器方法()的过程。此方法不需要定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,自动生成。

如果没有类变量和静态代码块,也不会有clinit

1.导致初始化的情况

main 方法所在的类,总会被首先初始化

首次使用这个类的静态方法或静态变量时会初始化

子类初始化,会一同进行父类的初始化

子类访问父类的静态变量,只会触发父类的初始化

Calss.forName

如:驱动加载

new 实例会导致类的初始化

2.不会导致初始化的情况 访问类的 static final 静态常量(上面的加载过程中的准备阶段加载的常量,基本数据类型和字符串常量)不会触发初始化调用类对象 .class 属性不会触发初始化创建该类的数组不会发生初始化调用类的 loadClass 方法不会初始化Class.forName 的设置第二个参数为 false 时不会初始化

最后就是使用和卸载阶段。

2.3 类加载器分类 2.3.1 系统自带

Bootstrap:启动类加载器

加载 Java 的基本的运行环境,Java 诞生时自带的类(基础类库)通过这个加载,如:object 类

Extension:扩展类加载器

除去基本的运行环境中含有的方法以外,在后续的更新中添加的一些额外扩展的类库,通过这个方式加载

AppClassLoader:应用程序类加载器

Java 也叫系统类加载器,加载当前应用的 classpath 的所有类,用于加载用户自定义的类

2.3.2 自定义加载器

概念上,将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义加载器。用户可以自己定义加载器的加载顺序和使用。(1)

(1):关于顺序,后面会讲解双亲委派机制,就可以了解到 JVM 内部的加载器的顺序是固定的且有要求的。

2.3.3 加载器顺序实例 public class ClassDemo { public static void main(String[] args) { Object o = new Object(); System.out.println(o.getClass().getClassLoader()); System.out.println("==========="); MyObject myObject = new MyObject(); System.out.println(myObject.getClass().getClassLoader().getParent().getParent()); System.out.println(myObject.getClass().getClassLoader().getParent()); System.out.println(myObject.getClass().getClassLoader()); } } class MyObject{ } // 结果 null =========== null sun.misc.Launcher$ExtClassLoader@1b6d3586 sun.misc.Launcher$AppClassLoader@18b4aac2

因为启动类加载器是通过 C++ 写的,无法获取,所以返回的是 null

Java 自带的类是没有父加载器的

加载顺序:启动类、扩展类、应用类加载器

2.4 双亲委派机制

Java 虚拟机对 Class 文件采用的是按需加载,而且加载 class 文件时,Java 虚拟机使用的是双亲委派模式,即把请求交由父类处理,它是异种任务委派模式。

当要执行一个类的时候,当前的类加载器不会马上执行,而是将这个类交给它的父类加载器执行,它的父类加载器也会交给它的父类执行,直到到顶层,如果顶层没有这个类,则会依次往下的加载器中寻找,在哪个加载器中找到就会用这个加载器中的名字相同的类。

通过双亲委派机制可以保证沙箱安全。(1)

(1):比如每一个用户 new 的 object 类都会是最顶层的启动类加载器中的 object,从而保证了,object的实例化时,无论是谁都只会从启动类加载器中获取到。

2.4.1 实例演示 // 伪装成基本运行环境下的包命 package java.lang; public class String { public static void main(String[] args) { } } // 运行结果: 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application

通过上面的代码我们就可以发现,虽然包路径相同,但是在执行时,因为双亲委派机制的存在,会首先从类加载器中获取,就会找寻 Java 基础类中 String 类的 main 方法,通过源码我们知道,肯定是没有的,因此会报错。

2.4.2 补充

在JVM中表示两个class对象,是否为同一个类存在两个必要条件:

类的完整类名必须一致,包括包名加载这个类的 ClassLoader 必须相同(1)

JVM 必须知道一个类型是由启动类加载器加载的,还是由用户类加载器加载的。如果是用户类加载器加载的,JVM会将这个类加载器的一个引用作为类型信息的一部分,保存到方法区中。

(1):即时路径全部相同,但是从两个不同的类加载器中加载出来,得到的两个类一定是不同的

2.5 线程上下文类加载器

通过双亲委派机制,我们知道了 JVM 中类加载顺序,那么什么时候需要自定义类加载器呢?

答案很明显,当我们需要按照自己的类加载顺序时,就需要改变 JVM 原有的类加载器顺序,如:Spring、TomCat等很多框架,都有自己的自定义类加载器,从 JVM 原有的类加载中派生出来,并且按照自己的加载器顺序来进行加载。

而破坏双亲委派机制就需要:线程上下文类加载器。

2.5.1 原理

jvm 创建线程的时候会将当前的应用程序加载器赋值给当前线程,所以可以通过 thread.getContextLoader();来获取到应用加载器。(也就是说获取到以后,不会再执行双亲委派,而是直接从当前路径下进行类加载),并且可以自己重写加载顺序。

3.程序计数器(PC寄存器) 3.1 概述

记录了方法之间指令的调用和执行情况,用来存储指向 JVM 下一条指令的地址

当前方法执行以后,存储指向下一个执行,保证执行的有序性

寄存器是 CPU 中读取速率最快的,JVM 就把 CPU 中寄存器当做计数器,来提升读取速度

不会出现内存溢出,没有垃圾回收

3.2 功能

当 CPU 在处理多个线程的时候,在 CPU 不停切换线程的过程中,可以用程序计数器来保存当前执行的位置,当 CPU 再次切换过来的时候,就可以直接从当前位置执行,避免重复执行。所以程序计数器是每一个线程单独拥有的。

如果正在执行的本地方法,这个计数器值则应为空。(undefined)

如果线程执行的Java方法,则计数器记录正在执行的虚拟机字节码的指令的地址

4.虚拟机栈 4.1 概述

线程运行需要的内存空间,管理线程的运行。Java 虚拟机栈,早起也叫 Java 栈,每个线程创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应着一次次的 Java 方法调用。生命周期和线程的一致。

主管Java程序的运行,保存方法的局部变量(8种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。

对于虚拟机栈来说最重要的就是栈帧。

4.2 栈帧(Stack Frame) 4.2.1 概述

用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈的基本元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。

最顶部的栈帧称为当前栈帧,栈帧所关联的方法称为当前方法,定义这个方法的类称为当前类,该线程中,虚拟机有且也只会对当前栈帧进行操作。

栈帧的作用有:存储数据和部分过程结果、处理动态链接、方法返回值和异常分派。

每一个栈帧包含的内容有局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。

在编译代码时,栈帧需要多大的局部变量表,多深的操作数栈都可以完全确定的,并写入到方法表的 code 属性中。(也就是说局部变量表和操作数栈在编译期就已经确定了大小)

执行引擎所有的字节码操作都是只针对与当前栈帧进行操作。

方法中的异常,会在当前方法中寻找是否有捕获异常的方式,如果没有就交给上级调用者,最终找到 main 方法中。

如果方法中调用了其他方法,对应的新的栈帧会被创建出来,放在顶端,成为新的当前帧。

4.2.2 内部结构

每一个栈帧包含的内容有局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。

1.局部变量表 定义为一个数字数组,主要用于存储方法参数,定义在方法体内部的局部变量,数据类型包括各类基本数据类型,对象引用,以及 return address 类型局部变量表建立在线程的栈上,是线程私有的,因此不存在数据安全问题局部变量表容量大小是在编译期确定下来的(1)局部变量表存放编译期可知的各种基本数据类型(8种),引用类型(reference)、return address 类型最基本的存储单元是 slot(2)32位占用一个 slot,64位类型(long和double)占用两个 slot局部变量表中的变量只有在当前方法调用中有效,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁方法嵌套的调用次数由栈的大小确定。每一个方法的栈帧的大小不一样,因此一个栈能存放栈帧(方法)的个数也和每一个栈帧的大小相关

(1):

2.关于 Slot

slot 是局部变量表的最基本的存储单位

在局部变量表中,32位以内的类型只占用一个 slot(包括returnAddress类型),64位的类型(long和double)占用两个slot

byte、short、char、在存储前被转化为int,boolean也被转换成int,0表示false,非0表示truelong和double则占据两个slot

JVM 会为每一个 slot 分配一个索引,通过这个索引就可以访问到局部变量表中的这个变量

当一个实例方法被调用时,这个实例方法中的局部变量表会按照顺序复制到调用者的局部变量表中的每一个slot上

64位的局部变量占用两个 slot,索引为占用第一个 slot 的下标

Slot 可以重复利用

栈帧中的局部变量表中的槽位(slot)是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后声明新的局部变量就很可能会用过期局部变量的槽位,从而到达节省资源的目的

3.操作数栈

在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈

如果被调用方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令

Java 虚拟机的解释引擎是基于栈的执行引擎,其中栈就是操作数栈

主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的

每一个操作数栈会拥有一个明确的栈深度,用于存储数值,最大深度在编译期就定义好

栈中,32bit 类型占用一个栈单位深度,64bit 类型占用两个栈单位深度

操作数栈并非采用访问索引方式进行数据访问,而是只能通过标准的入栈、出栈操作完成一次数据访问

栈顶缓存技术

由于操作数是存储在内存中,频繁的进行内存读写操作影响执行速度,将栈顶元素全部缓存到物理CPU的寄存器中,依此降低对内存的读写次数,提升执行引擎的执行效率

4.方法返回地址

存放调用该方法的pc寄存器的值

方法的结束

正常执行完成出现未处理异常,非正常退出

无论哪种方式退出,方法退出后,都会返回该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。 异常退出的,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息

本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

4.2.3 运行原理 不同线程中包含的栈帧不允许存在相互引用。当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为新的栈帧。Java方法有两种返回方式 一种是正常的函数返回,使用return指令另外一种是抛出异常,不管哪种方式,都会导致栈帧被弹出 4.3 优点

快速有效的存储方式,访问速度仅次于程序计数器

JVM 直接对 JAVA 栈的操作只有两个每个方法执行,伴随着进栈(入栈,压栈),执行结束的出栈

栈不存在垃圾回收,但是存在 OOM

Java 栈大小是动态或者固定不变的。如果是动态扩展,无法申请到足够内存 OOM,如果是固定,线程请求的栈容量超过固定值,则 StackOverflowError

4.4 静态变量和局部变量 4.4.1 按照类型分类

基本数据类型

引用数据类型

类、接口、数组

4.4.2 按照在类中声明的位置

成员变量

在使用前都经过默认初始化赋值

类变量(静态变量、静态代码块)

类的初始化阶段就会进行显示的赋值

实例变量

随着对象的创建会在堆空间进行实例变量赋值

局部变量

不会进行默认赋值,因此在使用前如果不进行赋值,就不会通过编译

public void test(){ int num; System.out.println(num); // 编译不通过,错误信息:num为赋值 } 4.4.3 关于为什么静态方法不能引用非静态变量?

通过前面的描述,在调用所有的构造方法或实例方法时,this 是占据第一个 slot 的,但是当这个实例方法是静态的时候,它就不会含有 this 这个变量,则它就不能引用类中的其他的非静态成员变量。

this:指当前类

5.本地方法栈、本地方法接口

当 Java 无法执行该方法,需要调用其他语言或需要和底层系统进行交互的时候,就会标记为 native(本地) 方法,native 方法在本地方法栈中登记(native method stack),在执行引擎执行的时候,通过 native interface 接口加载 native ibraies(本地方法库)

如今该方法使用越来越少,除非是与硬件相关的应用。

6.堆 6.1 概述

一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域

Java 堆区在 JVM 启动的时候即被创建,其空间大小也就确认了。堆内存的大小是可调节的

Java 虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的

所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区(TLAB)(1)

“几乎”所有的对象实例都在这里分配内存

逃逸分析:如果判断该对象在方法执行的过程到结束都不会逃离该方法,则可以直接在虚拟机栈上进行对象的额分配

数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,引用指向对象或者数组在堆中的位置

方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除

堆是 GC 执行垃圾回收的重点区域

1.为对象分配内存 TLAB(Thread Local Allocation Buffer) 堆区是线程共享区域,任何线程都可以访问到堆区的共享数据由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 6.2 空间分配

新生代

Eden 区

Survivor 区

from

to

谁空谁是to

老年代

元空间

运行时常量池

默认情况下,堆初始内存大小为:物理电脑内存大小/64。

6.3 新生代和老年代

根据对象的生命长度进行划分,对象初始都分配在新生代(除个别过大对象),当经历一定次数的“垃圾清理”会晋升到老年代。

默认空间大小比为 1:2,新生区和另外两个Survivor空间缺省所占的比例是:8:1:1。

6.4 对象分配过程

new 的对象先放在 Eden 区,此区有大小限制当创建新对象,Eden 空间填满时,会触发 Minor GC,将 Eden 不再被其他对象引用的对象进行销毁。再加载新的对象放到Eden区将 Eden 中剩余的对象移到幸存者0区再次触发垃圾回收,此时上次幸存者下来的,放在幸存者0区的,如果没有回收,就会放到幸存者1区再次经历垃圾回收,又会将幸存者重新放回幸存者0区,依次类推可以设置一个次数,默认是15次,到达经历“垃圾清理”15次以后会晋升到老年代 6.5 垃圾回收

MinorGC、FullGC,MinorGC 会进行新生代的垃圾清理,FullGC 会进行整个堆空间的垃圾清理。

6.5.1 MinorGC 的触发条件

当新生代空间不足时,就会触发 MinorGC,这里的新生代指的是 Eden 代满,Survivor 满不会触发GC。每次MinorGC 会清理新生代的内存。

因为 Java 中的对象大多朝生夕灭,所以 MinorGC 非常频繁。

6.5.2 FullGC 的触发机制 调用System.gc()时,系统建议执行FullGC,但是不必然执行老年代空间不足方法区空间不足通过 MinorGC 后进入老年代的平均大小,大于老年代的剩下可用内存由 Eden 区,Survivor 0 区向Survivor 1 区复制时,对象的大小大于交换空间的可用内存,则把改对象转存到老年代,且老年代的可用内存小于该对象的大小

FullGC 是开发或调优中尽量要避免的,对服务整体影响非常大。

7.方法区 7.1 概述 Java虚拟机规范中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现,可能不会选择去进行垃圾收集或者进行压缩。对于HotSpot而言,方法区还有一个别名叫Non-Heap(非堆),目的就是要和堆分开所以方法区看作是一块独立于 Java 堆的内存空间方法区和 Java 堆一样,是各个线程共享的内存区域方法区在 JVM 启动的时候被创建,并且它的实际的物理内存空间和 Java 堆区一样,都是可以不连续的方法区的大小和堆空间一样,可以选择固定大小或者可扩展方法区的大小决定了系统可以保存多少个类,如果定义太多类,加载大量的第三方的 Jar 包,Tomcat 部署过多工程,导致方法区溢出,虚拟机同样会抛出内存溢出 OOM:PermGen space或者Metaspace关闭 JVM 就会释放这个区域的内存 7.2 永久代和元空间

在 jdk7 及以前,习惯上把方法区,称为永久代,jdk8 开始,使用元空间取代了永久代

元空间的本质和永久带类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存 根据 JVM 规范,如果方法区无法满足新的内存分配需求,将抛出 OOM 异常

本质上,方法区和永久代并不等价,仅是对 HostSpot 而言的

Java 虚拟机规范,对如何实现方法区,不做统一要求

现在来看,当年使用永久代,不是好的点子,导致 Java 程序更容易 OOM

对于一个64位服务端 JVM 来说,默认的初始元数据区空间为21M,这就是初始的高水位线。一旦触及这个水位线,FULLGC 会触发并卸载没有用的类,然后高水位线会被重置。

新的高水位线的值取决于 GC 后释放了多少元空间。如果释放空间不足,在不超过最大设定值时,适当提高该值。如果释放空间过多,则适当降低该值。

7.2.1 永久代为什么被元空间替换 随着 JAVA 8 的到来,HotSpotVM 中再也见不到永久代了,但是并不意味着类的元数据信息也消失了,这些数据被转移到了一个与堆不相连的本地内存区域,这个区域叫做元空间 MetaSpace由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统的可用内存空间为永久代设置空间大小很难确定,在某些场景下,如果动态加载类过多,就容易产生 OOM而元空间并不在虚拟机中,而是使用本地内存,因此默认情况下,元空间的大小仅受本地内存限制对永久代进行调优是很困难的 7.3 内部结构

它用于存储已被虚拟机加载的类型信息、常量池、静态变量、方法信息,即时编译器编译后的代码缓存。

7.4 运行时常量池

运行时常量池是方法区的一部分

常量池表是 class 文件的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

在加载类和接口到虚拟机后,就会创建对应的运行时常量池

JVM 为每个已加载的类型都维护一个常量池,池中的数据像数组项一样,通过索引访问

运行时常量池包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后,才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里转换为真实地址。

运行时常量池,相对于 class 文件常量池的另一个重要特征是:具备动态性

例如:String.intern可以将字符串也放入运行时常量池

当创建类或接口的运行时常量池,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值。则JVM会抛出 OOM 异常

这里注意,常量池数量为 N,则索引为 1到 N-1

7.5 垃圾回收

常量池中废弃的常量、不再使用的类型。

常量池中废弃的常量

HotSpot 对常量池的回收策略很明确,只要常量池中的常量没有被任何地方引用,就可以被回收 回收废弃常量与回收Java堆中对象非常类似

不再使用的类型,需要同时满足三个条件:

该类所有的实例已经被回收 java 堆中不存在该类及其任何派生子类的实例 加载该类的类加载器已经被回收该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法

满足以上三个条件后,并不是和对象一样立即被回收,仅仅是允许。

底部


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3